Terminate message queries that exceed a configurable time limit - #5848
Conversation
b7e02a4 to
ec9cf12
Compare
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
Customer debug packages showed audit message queries against
MessagesViewIndexWithFullTextSearch running for up to ~65 minutes.
RavenDB's own Databases.QueryTimeoutInSec (default 300) does not stop
them because the server renews the operation deadline while a query
keeps making progress, so large scans/sorts run for hours and their
sorted results spill into the server's Temp folder, filling the disk.
The message view and search queries (all messages, per endpoint,
search, conversation) now run under a linked CancellationTokenSource
that cancels after a configurable time limit, default 1 minute, maximum
1 hour. Cancelling the client request aborts the call to the database
server, which does terminate the query server-side. On expiry a
TimeoutException names the setting to adjust; invalid values fall back
to the default.
One QueryTimeLimit helper in ServiceControl.Infrastructure serves every
persister, so they share one setting with one behavior, a hard
wall-clock deadline per data store call. Its rule is that whatever
exception surfaces after the deadline fired is the timeout: RavenDB and
Npgsql raise OperationCanceledException, Microsoft.Data.SqlClient raises
SqlException("Operation cancelled by user"), which a plain
OperationCanceledException catch never sees. A test against the real
SQL Server and PostgreSQL containers slows the command down server-side
to cover that.
- ServiceControl.Audit/QueryTimeoutInSeconds bounds the audit
RavenAuditDataStore message view queries. The licensing audit counts
and the saga history lookup are not under the limit.
- ServiceControl/QueryTimeoutInSeconds bounds the primary instance
IMessagesViewDataStore queries on RavenDB, SQL Server and PostgreSQL,
which use the same unbounded sorted index query shape. On SQL Server
and PostgreSQL these queries also use the limit as their per-command
timeout, so Database/CommandTimeout cannot undercut it.
Co-authored-by: Mauro Servienti <mauro.servienti@gmail.com>
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ec9cf12 to
e393787
Compare
|
@ramonsmits, I have not finished reviewing this yet. However, we also have this CustomCheck that probably needs to be adapted to incorporate the query timeouts. Also, we have this concept of an instance being temporarily unavailable if it fails to respond to the mentioned custom check. When that property is |
|
Looked at There was a real interaction though, caused by this PR: the probe used the remote's named 79d19e9 gives the probe its own budget independent of the query limit, makes a remote that doesn't answer in time fail the check and become temporarily unavailable, and lets a shutdown abort the probe without a verdict. Since your last pass I also dropped the 30 second margin (22e6109): the remote client now times out at this instance's limit, so |
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
mauroservienti
left a comment
There was a problem hiding this comment.
This is good to go, @ramonsmits 🚀
ServiceControl (Particular/ServiceControl#5848) now answers a partial composite with the X-Particular-Incomplete-Results header naming every instance whose data is missing (instanceId:timeout|unavailable|error), and a fully timed-out query with a 504. All Messages now reads both: a partial page keeps its data but shows a 'Partial results' warning naming each missing instance and its reason, and a 504 gets a precise 'exceeded the ServiceControl query time limit' message instead of the generic failure text. The header parser is shared so the conversation and saga views can adopt it next.
…data A data store's TimeoutException now becomes a 504 Gateway Timeout with a problem body naming the setting, on both the primary and the audit host, so a caller can tell a timeout from a crash and from an empty result. The primary's scatter-gather no longer hides what went wrong. A local timeout is absorbed the way a remote failure already was, a remote 504 or error is a missing instance rather than an instance with no data, and the response names every missing instance in the X-Particular-Incomplete-Results header (instanceId:timeout|unavailable| error) and carries no ETag. Only when no instance that was asked answered and one of them timed out does the composite itself fail with the timeout. The signal is a header rather than a body field because the composite endpoints return a bare array; an envelope would change the response schema and need a new API for every existing client. A remote's timeout does not cut the slower remotes short: the composite waits for every remote that is still answering and keeps their data. The in-process audit counts used for licensing throughput fail instead of recording a partial sum as the day's throughput. ServiceControl/QueryTimeoutInSeconds bounds the whole composite. The remote instance HttpClient times out at that limit instead of the 100 second default, so an audit instance that is slow, hung, or configured with a larger ServiceControl.Audit/QueryTimeoutInSeconds is reported as missing rather than stretching the response. The audit instance's own limit still ends the query on its side.
…ount The probe used the remote's HttpClient, whose timeout is now the query time limit, so a short limit failed the health check of a remote that was fine. The probe's own 10 second budget never applied: its cancellation was caught by a no-op branch, so a hanging remote passed the check and was never disabled, and a shutdown waited out the full budget. The probe now runs on its own budget regardless of the query time limit; a remote that does not answer in time is disabled and fails the check, and a shutdown aborts the probe without a verdict on the remote.
79d19e9 to
276acd7
Compare
ServiceControl (Particular/ServiceControl#5848) now tells clients when a composite result is incomplete: a partial answer stays HTTP 200 with the data that did arrive plus an X-Particular-Incomplete-Results header naming every instance that contributed nothing (instanceId:timeout| unavailable|error), and a query nobody answered becomes a 504. All Messages reads both. A partial page keeps its data and shows a warning naming each missing instance and why. The instance id is ServiceControl's base64 API URL, decoded and shown as host and port with the URL as tooltip; the warning says what to do: the rows shown come from the instances that did answer, try again in a moment, or check the listed instances on Platform Health (linked), and when one timed out, that a narrower time range makes the query lighter. The results line presents the total as a floor while results are partial ("of at least 87,421,337"). A 504 gets a precise "exceeded the ServiceControl query time limit" message with the existing one-click narrower-range escape hatches. The state is forgotten with the results when the view is left. The header parser is shared (incompleteResults.ts) so the conversation and saga views can adopt it next.
ServiceControl (Particular/ServiceControl#5848) now tells clients when a composite result is incomplete: a partial answer stays HTTP 200 with the data that did arrive plus an X-Particular-Incomplete-Results header naming every instance that contributed nothing (instanceId:timeout| unavailable|error), and a query nobody answered becomes a 504. All Messages reads both. A partial page keeps its data and shows a warning naming each missing instance and why. The instance id is ServiceControl's base64 API URL, decoded and shown as host and port with the URL as tooltip; the warning says what to do: the rows shown come from the instances that did answer, try again in a moment, or check the listed instances on Platform Health (linked), and when one timed out, that a narrower time range makes the query lighter. The results line presents the total as a floor while results are partial ("of at least 87,421,337"). A 504 gets a precise "exceeded the ServiceControl query time limit" message with the existing one-click narrower-range escape hatches. The state is forgotten with the results when the view is left. The header parser is shared (incompleteResults.ts) so the conversation and saga views can adopt it next.
Customer debug packages showed audit message queries against
MessagesViewIndexWithFullTextSearchrunning for up to ~65 minutes. RavenDB's ownDatabases.QueryTimeoutInSec(default 300) does not stop them because the server renews the operation deadline while a query keeps making progress, so large scans/sorts run for hours and their sorted results spill into the server's Temp folder, filling the disk.Query time limit
The message view and search queries now run under a linked
CancellationTokenSourcethat cancels after a configurable time limit, default 1 minute, maximum 1 hour. Cancelling the client request aborts the call to the database server, which does terminate the query server-side. Invalid values fall back to the default.One
QueryTimeLimithelper inServiceControl.Infrastructureserves all persisters. Its rule is that whatever exception surfaces after the deadline fired is the timeout: RavenDB and Npgsql raiseOperationCanceledException, Microsoft.Data.SqlClient raisesSqlException("Operation cancelled by user"). The SQL Server path is covered by a test against the real container that slows the command down server-side.The limit applies to exactly these queries:
RavenAuditDataStore:GetMessages,QueryMessages,QueryMessagesByReceivingEndpoint,QueryMessagesByReceivingEndpointAndKeyword,QueryMessagesByConversationId, read fromServiceControl.Audit/QueryTimeoutInSeconds.QueryAuditCounts(licensing throughput) andQuerySagaHistoryByIdare not under the limit.IMessagesViewDataStoreon RavenDB, SQL Server and PostgreSQL:GetAllMessages,GetAllMessagesForEndpoint,GetAllMessagesByConversation,GetAllMessagesForSearch,SearchEndpointMessages, read fromServiceControl/QueryTimeoutInSeconds. On SQL Server and PostgreSQL these queries also use the limit as their per-command timeout, soDatabase/CommandTimeoutcannot undercut it.Other paged index queries (
GetFailedMessages, groups, event log, ...) are unchanged.What a caller sees
504 Gateway Timeoutand aapplication/problem+jsonbody whose detail names the setting, on both hosts.504or error is a missing instance rather than an instance with no data. The response lists every missing instance in theX-Particular-Incomplete-Resultsheader (instanceId:timeout|unavailable|error, exposed through CORS) and carries noETag. Only when no queried instance answered and one of them timed out does the composite itself answer504.HttpClienttimeout isServiceControl/QueryTimeoutInSecondsplus 30 seconds instead of the 100 second default, so raising the limit on both instances does not make the primary give up on the audit instance first.ServicePulse follow-up: the audit list already treats a non-2xx answer (the new
504) as a failed query; readingX-Particular-Incomplete-Resultson a200is still to be added.Docs: Particular/docs.particular.net#8486